﻿/************************************************************************
 * Format of qrcode url c parameter:
 * 
 * UserIdEncrypt Version 1
 * -----------------------
 * 
 * byte 0: (1 byte)
 * ----------------
 * Unencoded: total mod 256 of unencoded bytes 1 - length, including signature and version
 * 
 * byte 1 - 4 (4 bytes)
 * --------------------
 * Encoded Encrypt signature
 * 
 * byte 5 - 8 (4 bytes)
 * --------------------
 * Encoded Encrypt version 
 * 
 * byte 9 - 28
 * -----------
 * Encoded userid as a string consisting of 10 unicode chars (e.g. 0000000005)
 * 
 * Encryption Algorithm
 * --------------------
 * 1. h = byte 0; (hashtotal)
 * 2. key = appsettings["EncryptKey"]
 * 3. i = 0;
 * for each byte from 1 to byte length:
	 * 4. add 12
	 * 5. mod 256
	 * 6. xor key[i]
	 * 7. xor hashtotal
	 * 8. i = (i + 1) mod key.length
	 * 9. hashtotal += 53
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Text;
using System.Web.Security;
using System.Security.Cryptography;

namespace Gollie
{
	public static class Hash
	{
		private static Dictionary<String, string> hashedStrings = new Dictionary<string, string>();
		private static Dictionary<String, uint> userIds = new Dictionary<string, uint>();
		private const int maxDictionarySize = 65536;
		private static uint userIdEncryptSignature = 0x3994B7E1;
		private static uint userIdEncryptVersion = 1;

		public static string GetHash(string input)
		{
			string res;
			if (hashedStrings.TryGetValue(input, out res) == false)
			{
				var sha512hasher = new SHA512Managed();
				byte[] data = sha512hasher.ComputeHash(Encoding.Default.GetBytes(input));

				res = Convert.ToBase64String(data);

				if (hashedStrings.Count < maxDictionarySize)
					hashedStrings[input] = res;
			}

			return res;
		}

		public static uint Decrypt(string encryptedUserId)
		{
			uint custId;
			if (userIds.TryGetValue(encryptedUserId, out custId) == false)
			{
				string decodedCustomerId = encryptedUserId.StartsWith("0000") ? encryptedUserId : Hash.DecryptToString(Convert.FromBase64String(HttpUtility.UrlDecode(encryptedUserId)));

				if (string.IsNullOrEmpty(decodedCustomerId) || (decodedCustomerId.Length != 10 && decodedCustomerId.Length != 8) || uint.TryParse(decodedCustomerId, out custId) == false)
					custId = uint.MaxValue;
				else
					if (userIds.Count < maxDictionarySize)
						userIds[encryptedUserId] = custId;
			}

			return custId;
		}

		public static string GetMD5Hash(string input)
		{
			MD5CryptoServiceProvider md5Hasher = new MD5CryptoServiceProvider();

			byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(input));

			StringBuilder sBuilder = new StringBuilder();

			for (int i = 0; i < data.Length; i++)
			{
				sBuilder.Append(data[i].ToString("x2"));
			}

			return sBuilder.ToString();
		}

		public static string GetRandomPassword()
		{
			int length = 6 + (new Random().Next(3));

			byte[] random = new byte[length];

			StringBuilder newPassword = new StringBuilder();

			RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider();

			crypto.GetBytes(random);

			for (int i = 0; i < length; i++)
			{
				int rand = Convert.ToInt32(random[i]) % 26;

				newPassword.Append(Convert.ToChar('a' + rand));
			}

			return newPassword.ToString();
		}

		internal static string Encrypt(string str)
		{
			//------------------length of userid (10 chars) -- signature --- version ------- sum of all bytes mod 256
			byte[] b = new byte[str.Length * sizeof(char) + sizeof(uint) + sizeof(uint) + sizeof(byte)];

			int offset = 1; //keep first byte for sum
			byte[] signature = BitConverter.GetBytes(userIdEncryptSignature);
			byte[] version = BitConverter.GetBytes(userIdEncryptVersion);
			byte[] userId = new byte[str.Length * sizeof(char)];
			Buffer.BlockCopy(str.ToCharArray(), 0, userId, 0, userId.Length);

			Buffer.BlockCopy(signature, 0, b, offset, signature.Length);
			offset += signature.Length;

			Buffer.BlockCopy(version, 0, b, offset, version.Length);
			offset += version.Length;

			Buffer.BlockCopy(userId, 0, b, offset, userId.Length);

			b[0] = GetHashedSum(b, 1);

			DoKey(b);

			return Convert.ToBase64String(b);
		}

		internal static byte[] DecryptToBytes(byte[] byteArray)
		{
			byte[] b = new byte[byteArray.Length];
			Buffer.BlockCopy(byteArray, 0, b, 0, b.Length);

			DoUnKey(b);

			return b;
		}

		internal static string DecryptToString(byte[] byteArray)
		{
			byte[] b = DecryptToBytes(byteArray);

			return GetValidatedDecryptedUserId(b);
		}

		private static byte GetHashedSum(byte[] b, int startIndex)
		{
			byte s = 0;
			for (int i=startIndex; i < b.Length; i++) s += b[i];
			return s;
		}

		private static string GetValidatedDecryptedUserId(byte[] b)
		{
			string retVal = null;

			if (b.Length < 16)
				return null;

			uint version = 0;
			uint expectedSignature = BitConverter.ToUInt32(b, 1);
			if (expectedSignature == userIdEncryptSignature)
			{
				version = BitConverter.ToUInt32(b, 5);
				switch (version)
				{
					case 1: retVal = GetValidatedDecryptedUserIdV1(b);
						break;
				}
			}
			else
				retVal = Encoding.Unicode.GetString(b);

			return retVal;
		}

		private static string GetValidatedDecryptedUserIdV1(byte[] b)
		{
			byte s = GetHashedSum(b, 1);
			if (s != b[0])
				return null;

			int prefixSize = 2 * sizeof(uint) + 1;
			return Encoding.Unicode.GetString(b, prefixSize, b.Length - prefixSize);
		}

		private static void DoKey(byte[] b)
		{
			byte[] key = Convert.FromBase64String(ConfigAppSettings.GetValue("EncryptKey"));
			int keyI = 0;

			//skip first byte (sum mod 256)
			for (int i = 1; i < b.Length; i++)
			{
				byte b1 = (byte)( (b[i] + 12) & 255);
				b[i] = (byte)(b1 ^ key[keyI]);

				keyI++;
				if (keyI == key.Length)
					keyI = 0;
			}

			DoKeyStep2(b);
		}

		private static void DoKeyStep2(byte[] b)
		{
			switch (userIdEncryptVersion)
			{
				case 1: DoKeyStep2V1(b);
					break;
			}
		}

		private static void DoKeyStep2V1(byte[] b)
		{
			byte hashTotal = b[0];
			for (int i = 1; i < b.Length; i++)
			{
				b[i] ^= hashTotal;
				hashTotal += 53;
			}
		}

		private static void DoUnKey(byte[] b)
		{
			int startIndex = 0;
			if (b.Length >= 29)
			{
				startIndex = 1;
				switch (userIdEncryptVersion)
				{
					case 1: DoUnkeyStep1V1(b);
						break;
				}
			}
			byte[] key = Convert.FromBase64String(ConfigAppSettings.GetValue("EncryptKey"));
			int keyI = 0;

			for (int i = startIndex; i < b.Length; i++)
			{
				int b1 = (b[i] ^ key[keyI]);

				b[i] = (byte)(b1 < 12 ? b1 + 244 : b1 - 12);

				keyI++;
				if (keyI == key.Length)
					keyI = 0;
			}
		}

		private static void DoUnkeyStep1V1(byte[] b)
		{
			byte hashTotal = b[0];
			for (int i = 1; i < b.Length; i++)
			{
				b[i] ^= hashTotal;
				hashTotal += 53;
			}
		}
	}
}